﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Audio;

namespace theguineasyndrome
{
    public class Character : ILevelObject
    {
        // state
        public enum State
        {
            IDLE   = 0,
            RUN    = 1,
            JUMP   = 2,
            FALL   = 3,
            CUSTOM = 4
        };

        private State state;

        // model
        private CharacterModel model;

        // animation
        private Animation animation;

        // position
        private Vector2 position;

        private int direction;

        // arm angle
        private float armAngle;

        // physics
        private Vector2 speed;
        private float modifiedSpeed;
        private ObstacleElement obstacleElement;

        // weapon
        private Weapon weapon;
        private int primaryElapsedTime;
        private int secondaryElapsedTime;

        // portal
        private Portal currentPortal;

        // carot
        private Carot lastCarot;

        // random
        private Random random;

        // level
        private Level level;

        // clouds
        private Clouds clouds;

        // appmode used as input helpers
        private GameMode gameMode;

        // score
        private Score score;

        // recharge time texture
        private Texture2D redbarTexture;

        // sound played when a carot is collected
        private SoundEffect carotSound;

        // sound played when killing an enemy
        private SoundEffect enemySound;

        // rasterizer used while in a portal
        protected static RasterizerState scissor = new RasterizerState() { ScissorTestEnable = true };
        protected static Rectangle voidRectangle = new Rectangle();

        // dimensions
        public float Width
        {
            get { return model.Width; }
        }
        public float Height
        {
            get { return model.Height; }
        }

        public float Size
        {
            get { return Height; }
        }

        public float SquareLeft
        {
            get { return X - Size / 2; }
        }

        public float SquareRight
        {
            get { return X + Size / 2; }
        }

        public float SquareTop
        {
            get { return Y - Size; }
        }

        public float SquareBottom
        {
            get { return Y; }
        }

        public float Left
        {
            get { return position.X - Width / 2; }
        }

        public float Right
        {
            get { return position.X + Width / 2; }
        }

        public float Top
        {
            get { return position.Y - Height; }
        }

        public float Bottom
        {
            get { return position.Y; }
        }

        public float Depth
        {
            get { return Bottom; }
        }

        public Rectangle HitBox
        {
            get { return new Rectangle((int)Left, (int)Top, (int)Width, (int)Height); }
        }

        public float X
        {
            get { return position.X; }
        }
        public float Y
        {
            get { return position.Y; }
        }
        public Vector2 Position
        {
            get { return position; }
        }

        public Vector2 Center
        {
            get { return new Vector2((Right + Left) / 2, (Bottom + Top) / 2); }
        }

        public float CenterX
        {
            get { return (Right + Left) / 2; }
        }

        public float CenterY
        {
            get { return (Bottom + Top) / 2; }
        }

        public Vector2 Speed
        {
            get { return speed; }
            set { speed = value; }
        }

        public float SpeedX
        {
            get { return speed.X; }
            set { speed.X = value; }
        }

        public float SpeedY
        {
            get { return speed.Y; }
            set { speed.Y = value; }
        }

        public float ModifiedSpeed
        {
            get { return modifiedSpeed; }
            set { modifiedSpeed = value; }
        }

        public State CurrentState
        {
            get { return state; }
        }

        public float ArmAngle
        {
            get { return armAngle; }
        }

        public Character(GameMode gameMode, Level level, Clouds clouds, Vector2 position, CharacterModel model, Score score, Texture2D redbarTexture, SoundEffect carotSound, SoundEffect enemySound)
        {
            this.gameMode = gameMode;
            this.level = level;
            this.clouds = clouds;
            this.score = score;
            this.redbarTexture = redbarTexture;
            this.carotSound = carotSound;
            this.enemySound = enemySound;
            setModel(model);
            setPosition(position);
            setState(State.IDLE);
            setObstacleElement(null);
            primaryElapsedTime = 0;
            direction = 1;
            random = new Random();
        }

        private void setModel(CharacterModel model)
        {
            this.model = model;
            model.Character = this;

            modifiedSpeed = model.getSpeed();

            Animation newAnimation = model.Animation;

            if (animation != null)
                newAnimation.copyState(animation);

            animation = newAnimation;

            weapon = model.Weapon;

            primaryElapsedTime = 0;
            secondaryElapsedTime = 0;
        }

        public float primaryRechargeTime()
        {
            if (primaryElapsedTime > weapon.getPrimaryPeriod())
                return 1;

            else
                return (float)primaryElapsedTime / weapon.getPrimaryPeriod();
        }

        public float secondaryRechargeTime()
        {
            if (secondaryElapsedTime > weapon.getSecondaryPeriod())
                return 1;

            else
                return (float)secondaryElapsedTime / weapon.getSecondaryPeriod();
        }

        public void Update(GameTime gameTime, Vector2 mouse)
        {
            //animation.Update(gameTime);
            model.Update(gameTime);

            modifiedSpeed -= (float)(gameTime.ElapsedGameTime.TotalMilliseconds * model.getDeceleration());
            if (modifiedSpeed < model.getSpeed())
                modifiedSpeed = model.getSpeed();

            if (currentPortal == null || currentPortal.getSide() == LevelElement.Side.TOP || currentPortal.getSide() == LevelElement.Side.BOTTOM)
            {
                bool left = gameMode.isPressed(Keys.Left);
                bool right = gameMode.isPressed(Keys.Right);
                bool up = gameMode.isPressed(Keys.Up);

                if (left && !right)
                    move(-1);

                else if (right && !left)
                    move(1);
    
                if (!left && !right)
                    stopMoving();

                if (up)
                    jump();
            }

            updatePosition(gameTime);
            avoidObstacles();
            avoidBorders();
            collectCarot(gameTime);
            hitEnemies(gameTime);

            if (weapon != null)
            {
                // mouse
                bool leftClickPressed = gameMode.isPressed("LeftButton");
                bool rightClickPressed = gameMode.isPressed("RightButton");
                bool leftClickJustPressed = gameMode.isJustPressed("LeftButton");
                bool rightClickJustPressed = gameMode.isJustPressed("RightButton");

                // fire source position
                Vector2 shoulderPosition = position + model.getShoulderPosition();
                Vector2 fireSource = position + model.getFireSource();
                Vector2 fireSpeed = new Vector2();

                fireSpeed.X = mouse.X - shoulderPosition.X;
                fireSpeed.Y = mouse.Y - shoulderPosition.Y;
                fireSpeed /= 300;

                if (fireSpeed.LengthSquared() > 1)
                    fireSpeed *= 1 / fireSpeed.Length();

                armAngle = Utils.PolarCoordinates.fromCartesian(fireSpeed).Y;

                if (currentPortal == null || !(weapon is PortalWeapon))
                {
                    bool primaryRequiresJustClicked = weapon.primaryRequiresJustClicked();
                    bool secondaryRequiresJustClicked = weapon.secondaryRequiresJustClicked();

                    bool triggerPrimary = (leftClickPressed && !primaryRequiresJustClicked) || (leftClickJustPressed && primaryRequiresJustClicked);
                    fire(gameTime, fireSource, fireSpeed, ref primaryElapsedTime, weapon.getPrimaryPeriod, weapon.firePrimary, triggerPrimary);

                    bool triggerSecondary = (rightClickPressed && !secondaryRequiresJustClicked) || (rightClickJustPressed && secondaryRequiresJustClicked);
                    fire(gameTime, fireSource, fireSpeed, ref secondaryElapsedTime, weapon.getSecondaryPeriod, weapon.fireSecondary, triggerSecondary);
                }
            }
        }

        private void collectCarot(GameTime gameTime)
        {
            Carot carot = level.getCollidingCarot(HitBox);
            if (carot != null)
            {
                carotSound.Play(0.4f, 0, 0);
                level.removeElement(carot);
                if (carot is EndCarot)
                {
                    gameMode.finishLevel();
                }
                else if (carot.CharacterModel != null)
                {
                    if (lastCarot != null)
                        level.addElement(lastCarot);

                    setModel(carot.CharacterModel);

                    for (int i = 0; i < 10; i++)
                        clouds.add(Center + new Vector2((float)(random.NextDouble() - 0.5) * 90, (float)(random.NextDouble() - 0.5) * 90));

                    lastCarot = carot;
                }
                else
                    score.addCarot();
            }
        }

        public void die()
        {
            for (int i = 0; i < 10; i++)
                clouds.add(Center + new Vector2((float)(random.NextDouble() - 0.5) * 90, (float)(random.NextDouble() - 0.5) * 90));

            gameMode.over();
        }

        private void hitEnemies(GameTime gameTime)
        {
            Enemy enemy = level.getCollidingEnemy(HitBox);
            if (enemy != null)
            {
                if (speed.Y > 0 && enemy.CollisionLeft <= Right && Left <= enemy.CollisionRight && enemy.CollisionBottom > Bottom)
                {
                    enemySound.Play();
                    clouds.add(enemy.Center);
                    level.removeElement(enemy);
                    speed.Y = -5;
                }
                else
                {
                    die();
                }
            }
        }

        private void avoidObstacles()
        {
            bool moveUp = false;
            bool moveDown = false;
            bool moveLeft = false;
            bool moveRight = false;

            float oldX = position.X;

            List<Utils.Pair<ObstacleElement, Rectangle>> collidingObstacles = level.getCollidingObstacles(HitBox);

            foreach (Utils.Pair<ObstacleElement, Rectangle> collidingObstacle in collidingObstacles)
            {
                ObstacleElement obstacle = collidingObstacle.First;
                Rectangle intersection = collidingObstacle.Second;

                obstacle.repulseCharacterX(this, intersection, ref moveLeft, ref moveRight);
            }

            collidingObstacles = level.getCollidingObstacles(HitBox);

            foreach (Utils.Pair<ObstacleElement, Rectangle> collidingObstacle in collidingObstacles)
            {
                ObstacleElement obstacle = collidingObstacle.First;
                Rectangle intersection = collidingObstacle.Second;

                obstacle.repulseCharacterY(this, intersection, ref moveUp, ref moveDown);
            }

            if (!moveUp)
                setObstacleElement(null);

            /*
            if (moveUp && moveDown || moveLeft && moveRight)
                animation.setColor(Color.Red);
            */
        }

        private void avoidBorders()
        {
            if (Left < 0)
                setPosition(new Vector2(Width / 2, position.Y));

            else if (Right > level.Right)
                setPosition(new Vector2(level.Right - Width / 2, position.Y));

            if (Top - 100 > level.Bottom)
                die();
        }

        public void setObstacleElement(ObstacleElement obstacleElement)
        {
            if (this.obstacleElement != null)
            {
                this.obstacleElement.notifyCharacterAway();
            }

            if (obstacleElement != null)
            {
                obstacleElement.notifyCharacterOver(this);
            }

            this.obstacleElement = obstacleElement;
        }

        public void Draw(SpriteBatch spriteBatch, Camera camera)
        {
            if (currentPortal != null)
            {
                spriteBatch.End();
                spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, null, null, scissor);

                switch (currentPortal.getSide())
                {
                    case LevelElement.Side.LEFT:
                    {
                        int width = (int)(camera.getRelative(new Vector2(currentPortal.CenterX, 0)).X - camera.Left * 2);

                        if (width > 0 && width <= camera.Width)
                            spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(0, 0, width, (int)camera.Height);

                        else
                            spriteBatch.GraphicsDevice.ScissorRectangle = voidRectangle;
                    }
                    break;

                    case LevelElement.Side.RIGHT:
                    {
                        int x = (int)(camera.getRelative(new Vector2(currentPortal.CenterX, 0)).X - camera.Left * 2);
                        int width = (int)(camera.Width - x);

                        if (width > 0 && width <= camera.Width)
                            spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(x, 0, width, (int)camera.Height);

                        else
                            spriteBatch.GraphicsDevice.ScissorRectangle = voidRectangle;
                    }
                    break;

                    case LevelElement.Side.TOP:
                    {
                        int height = (int)(camera.getRelative(new Vector2(0, currentPortal.CenterY)).Y - camera.Top * 2);

                        if (height > 0 && height <= camera.Height)
                            spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(0, 0, (int)camera.Width, height);

                        else
                            spriteBatch.GraphicsDevice.ScissorRectangle = voidRectangle;
                    }
                    break;

                    case LevelElement.Side.BOTTOM:
                    {
                        int y = (int)(camera.getRelative(new Vector2(0, currentPortal.getElement().Bottom)).Y - camera.Top * 2);
                        int height = (int)(camera.Height - y);

                        if (height > 0 && height <= camera.Height)
                            spriteBatch.GraphicsDevice.ScissorRectangle = new Rectangle(0, y, (int)camera.Width, height);

                        else
                            spriteBatch.GraphicsDevice.ScissorRectangle = voidRectangle;
                    }
                    break;
                }
            }

            model.Draw(spriteBatch, camera);

            if (currentPortal != null)
            {
                spriteBatch.End();
                spriteBatch.Begin();
            }
        }

        public void DrawRechargeTimes(SpriteBatch spriteBatch, Camera camera)
        {
            if (weapon != null)
            {
                if (!weapon.secondaryRequiresJustClicked() && weapon.getSecondaryPeriod() > 0)
                {
                    Rectangle rect = new Rectangle((int)(camera.Width - 160), 80, (int)(150 * secondaryRechargeTime()), 10);
                    spriteBatch.Draw(redbarTexture, rect, Color.White);
                }
            }
        }

        public void move(float direction)
        {
            if (obstacleElement != null)
            {
                speed.X = modifiedSpeed * direction;

                if (direction < 0)
                {
                    animation.flip(true, false);
                    this.direction = -1;
                }
                else
                {
                    animation.flip(false, false);
                    this.direction = 1;
                }

                setState(State.RUN);
            }
            else
            {
                float maxSpeed = modifiedSpeed;

                if (speed.X >= maxSpeed && direction < 0 || speed.X <= -maxSpeed && direction > 0)
                {
                    speed.X += direction;
                }
                else if (Math.Abs(speed.X) < maxSpeed)
                {
                    speed.X += direction;

                    if (speed.X < -maxSpeed)
                        speed.X = -maxSpeed;

                    else if (speed.X > maxSpeed)
                        speed.X = maxSpeed;
                }
            }
        }

        public void jump()
        {
            if (obstacleElement != null)
            {
                speed.Y = -model.getJumpForce();
                setObstacleElement(null);
                setState(State.JUMP);
            }
        }

        public void stopMoving()
        {
            if (obstacleElement != null)
            {
                speed.X = 0;
                modifiedSpeed = model.getSpeed();
                setState(State.IDLE);
            }
        }

        public int getDirection()
        {
            return direction;
        }

        private delegate float WeaponPeriod();
        private delegate Vector2 WeaponFire(int time, Vector2 position, Vector2 speed);

        private void fire(GameTime gameTime, Vector2 sourcePosition, Vector2 fireSpeed, ref int elapsedTime, WeaponPeriod weaponPeriod, WeaponFire weaponFire, bool trigger)
        {
            elapsedTime += (int)gameTime.ElapsedGameTime.TotalMilliseconds;
            if (trigger && elapsedTime > weaponPeriod())
            {
                elapsedTime = 0;

                Vector2 recoil = weaponFire((int)gameTime.TotalGameTime.TotalMilliseconds, sourcePosition, fireSpeed);

                if (obstacleElement == null)
                    speed += recoil;
            }
        }

        public void setPosition(Vector2 position)
        {
            this.position = position;
            animation.setPosition(position);
        }

        private void setState(State state)
        {
            if (this.state != state)
            {
                this.state = state;
                animation.setFrameLine((int)state);
            }
        }

        private void updatePosition(GameTime gameTime)
        {
            if (obstacleElement == null)
            {
                speed.Y += (float)gameTime.ElapsedGameTime.TotalMilliseconds * model.getWeight();
                if (speed.Y > 0)
                    setState(State.FALL);
            }
            else
            {
                speed.Y = 0;

                Splash splash = obstacleElement.getTopSplashAt(Left, Right);
                if (splash != null)
                    splash.applyEffect(gameTime, this);
            }

            float maxSpeed = CharacterModel.MAX_SPEED;
            if (speed.LengthSquared() > maxSpeed * maxSpeed)
            {
                speed.Normalize();
                speed *= maxSpeed;
            }

            setPosition(position + speed);

            if (currentPortal != null)
            {
                currentPortal.repulseFromBorders(this);

                if (!currentPortal.isInside(this))
                    setCurrentPortal(null);
            }
            else
            {
                animation.setHeightFromTop((int)animation.Height);
            }
        }

        public void setWeapon(Weapon weapon)
        {
            this.weapon = weapon;
        }

        public Weapon getWeapon()
        {
            return weapon;
        }

        public void queueDrawing(Camera camera)
        {
            // always visible
            level.queueObjectDrawing(this);
        }

        public void setCurrentPortal(Portal portal)
        {
            /*
            if (portal == null && currentPortal != null)
                Console.WriteLine("leaving {0} portal", currentPortal.getColor());

            else if (portal != null && currentPortal != portal)
                Console.WriteLine("entering {0} portal", portal.getColor());
            */
            currentPortal = portal;
        }

        public Portal getCurrentPortal()
        {
            return currentPortal;
        }

        public CharacterModel getModel()
        {
            return model;
        }
    }
}
